在過去的幾天裡,我們成功地實作家用品清單的新增和刪除功能。今天,我們要進一步讓這些資料不再只是暫時存在記憶體中,而是能夠永久儲存。為了實現這個目標,我們將學習如何在 SwiftUI 中使用 Core Data,將資料儲存在本地的手機中。
Core Data 是蘋果提供的一個強大框架,用於管理本地資料的存取。它不僅僅是資料庫,而是一個物件圖形管理工具,可以幫助我們有效地管理 App 中的資料。我們可以用它來儲存結構化的資料,並且透過一系列 API 來輕鬆操作這些資料。
要在 SwiftUI 中使用 Core Data,我們首先需要在專案中加入 Core Data 支援。還記得在 Day2 時,我們在 Xcode 中建立專案,選擇加入 Core Data 功能。如果當初沒有一起選擇這個選項,也可以手動將 Core Data 整合進來。
首先,檢查專案是否已經包含 Core Data 支援。如果還沒有,可以手動增加一個 xcdatamodeld 檔,這個檔案將作為我們的資料模型。
如果當初建立專案時有加入 Core Data ,會像我一樣有一個 專案名+.xcdatamodeld 的檔案。
開啟 xcdatamodeld 檔,然後在其中建立一個名為 Item 的實體。這個實體會包含我們要儲存的屬性,例如 name(名稱)和 quantity(數量)。
不過我們的家用品庫存 App 可不只是這麼簡單!除了記錄家用品外,我們還會提供到期日提醒、記帳、以及紀錄物品使用狀況等功能。因此,我們需要創建以下欄位:
可能有人會疑惑為什麼需要 isUsedUp 和 usedQuantity 這兩個欄位。這是因為當物品用完後,如果直接刪除資料,可能會影響到我們的記帳記錄。所以不能直接從資料庫中刪除物品。
新增完成後,畫面應該會顯示成這樣。
在這個步驟中,我們引入了一個新的類別 DataManager,用來集中管理所有與 Core Data 相關的操作。DataManager 負責處理 CRUD(Create、Read、Update、Delete)操作,並將資料存取邏輯與 UI 邏輯分離,這樣可以讓我們的代碼結構更加清晰、模組化。
DataManager 是一個類別,這代表整個 App 都可以共用這個物件,保持資料的一致性。它將 NSPersistentContainer
封裝在 DataManager 中,負責 Core Data 的初始化和資料的保存。通過這樣的設計,我們的 ViewModel 可以更專注於 UI 層的邏輯,而不需要直接處理 Core Data 的細節。這也讓原本預設的 PersistenceController
可以刪除了。
import Foundation
import CoreData
class DataManager: NSObject, ObservableObject {
// 初始化 Core Data 的容器,名稱與 xcdatamodeld 檔案一致
let container: NSPersistentContainer = NSPersistentContainer(name: "YourName")
override init() {
super.init()
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
// MARK: - CRUD Operations
// Read (Fetch)
func fetchItems() -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
do {
return try container.viewContext.fetch(request)
} catch {
print("Failed to fetch items: \(error)")
return []
}
}
// Create
func addItem(name: String, quantity: Int) {
let newItem = Item(context: container.viewContext)
newItem.id = UUID()
newItem.name = name
newItem.quantity = Int16(quantity)
newItem.isUsedUp = false
newItem.dateAdded = Date()
newItem.price = 0.0
newItem.usedQuantity = 0
saveContext()
}
// Update
func updateItem(item: Item, name: String, quantity: Int) {
item.name = name
item.quantity = Int16(quantity)
saveContext()
}
// Delete
func deleteItems(at offsets: IndexSet, items: [Item]) {
offsets.map { items[$0] }.forEach(container.viewContext.delete)
saveContext()
}
// Save Context
private func saveContext() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
目前我們在新增項目時,只有兩個輸入欄位 - 名稱和數量,因此在 addItem 函式中,暫時將未設置的欄位使用預設值,這樣可以避免執行時發生閃退。明天,我們將補齊這些欄位,並改寫這部分的程式碼,讓我們的 App 更加完善!
因為我們的資料來自 Core Data,所以我們需要更新 ItemViewModel,才能與 Core Data 進行互動。我們將使用 DataManager 來管理資料的儲存和刪除。
import SwiftUI
class ItemViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var newItemName: String = ""
@Published var newItemQuantity: String = ""
private let dataManager: DataManager
init(dataManager: DataManager = DataManager()) {
self.dataManager = dataManager
fetchItems()
}
func fetchItems() {
items = dataManager.fetchItems()
}
func addItem(name: String, quantity: Int) {
dataManager.addItem(name: name, quantity: quantity)
fetchItems()
}
func deleteItems(at offsets: IndexSet) {
dataManager.deleteItems(at: offsets, items: items)
fetchItems()
}
}
我們將剛剛編寫的 DataManager 引入到專案中,讓基本的 CRUD 操作都交由 DataManager 處理,而不需要在 ViewModel 中直接處理。
在程式碼中,init(dataManager: DataManager = DataManager())
是一個依賴注入的小技巧,這樣做的目的是讓未來撰寫單元測試更方便。至於什麼是依賴注入,我們會在後面的章節中介紹,敬請期待!
在 ContentView 中,我們將使用 ItemViewModel 來管理和顯示資料,讓所有資料的變動都能更新在 UI 上。
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel: ItemViewModel = ItemViewModel()
var body: some View {
NavigationView {
VStack {
HStack {
TextField("輸入家用品名稱", text: $viewModel.newItemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("數量", text: $viewModel.newItemQuantity)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
viewModel.addItem(name: viewModel.newItemName, quantity: Int(viewModel.newItemQuantity) ?? 0)
}) {
Text("新增")
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
List {
ForEach(viewModel.items) { item in
HStack {
Text(item.name)
Spacer()
Text("數量: \(item.quantity)")
}
}
.onDelete(perform: viewModel.deleteItems)
}
.navigationTitle("家用品清單")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let dataManager = DataManager()
let viewContext = dataManager.container.viewContext
let viewModel = ItemViewModel(dataManager: dataManager)
return ContentView()
.environment(\.managedObjectContext, viewContext)
}
}
在更新 App 的入口,我們需要將 DataManager 注入到 SwiftUI 的環境中,這樣在整個 App 中都可以方便地使用 Core Data 的 viewContext。具體程式碼如下:
import SwiftUI
@main
struct HandyInventory_ironApp: App {
let dataManager = DataManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataManager.container.viewContext)
}
}
}
在這段程式碼中,我們做了以下幾件事:
.environment(\.managedObjectContext, dataManager.container.viewContext)
,將 DataManager 的 viewContext 注入到 SwiftUI 的環境中。這樣,我們的 ContentView 以及其子畫面都能夠透過 @Environment(\.managedObjectContext)
獲取這個上下文,並進行資料操作。這樣設置之後,我們的 App 就可以在各個畫面中輕鬆使用 Core Data 進行資料的存取和管理。
通過以上步驟,我們將資料儲存到了 Core Data 中,這意味著即使 App 關閉,這些家用品項目也會持續存在。當 App 重新打開時,這些資料將會自動從 Core Data 中載入並顯示在清單中。讓我們來看看成果吧!
這樣一來,我們的 App 不再是暫時性的清單,而是一個能夠永久儲存資料的實用工具。透過 DataManager 的整合,我們可以簡單地管理 Core Data 的操作,並將資料存儲邏輯與 UI 邏輯分離。今天先寫到這裡,我們明天見!
參考資料:SwiftUI & Core Data